package com.leyou.search.service.impl;

import com.leyou.common.dto.PageDTO;
import com.leyou.common.exception.LyException;
import com.leyou.item.client.ItemClient;
import com.leyou.item.dto.SkuDTO;
import com.leyou.item.dto.SpecParamDTO;
import com.leyou.item.dto.SpuDTO;
import com.leyou.search.dto.SearchParamDTO;
import com.leyou.search.entity.ElasticConstants;
import com.leyou.search.entity.Goods;
import com.leyou.search.repository.GoodsRepository;
import com.leyou.search.service.SearchService;
import com.leyou.starter.elastic.dto.PageInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

import java.util.*;
import java.util.stream.Collectors;

import static com.leyou.search.constants.SearchConstants.*;


@Slf4j
@Service
public class SearchServiceImpl implements SearchService {

    private final GoodsRepository goodsRepository;

    private final ItemClient itemClient;

    public SearchServiceImpl(GoodsRepository goodsRepository, ItemClient itemClient) {
        this.goodsRepository = goodsRepository;
        this.itemClient = itemClient;
    }


    //升序flase，


    @Override
    public void createIndexAndMapping() {
        // 删除已经存在的索引库
        try {
            goodsRepository.deleteIndex();
        } catch (Exception e) {
            log.error("删除失败，可能索引库不存在！", e);
        }
        // 然后创建一个新的
        goodsRepository.createIndex(ElasticConstants.SOURCE_TEMPLATE);
    }


    @Override
    public void loadData() {
        //SpuDTO(id=39, brandId=15127, cid1=74, cid2=75, cid3=76,
        // name=三星 Galaxy S9+, title=三星 Galaxy S9+（SM-G9650/DS）6GB+128GB ,
        // saleable=true, categoryName=手机/手机通讯/手机, brandName=三星（SAMSUNG）,
        // spuDetail=null, skus=null)
        int page = 1, rows = 10;
        while (true) {
            log.info("开始导入第{}页数据", page);
            // 分页查询已经上架的spu
            PageDTO<SpuDTO> result = itemClient.querySpuByPage(page, rows, true, null, null, null);
            List<SpuDTO> list = result.getItems();

            // 遍历Spu集合，把SpuDTO通过buildGoods方法转为Goods 并且为其中的属性赋值
            //我们仅仅将它用作查询后对页面的一个展示，所以只查询goods里面的内容即可
            List<Goods> goodsList = list.stream()
                    .map(this::buildGoods).collect(Collectors.toList());

            System.out.println("goodsList ================== " + goodsList.size());
            // 批量写入Elasticsearch
            goodsRepository.saveAll(goodsList);
            log.info("导入第{}页数据结束。", page);
            // 翻页
            page++;

            // 获取总页数
            Long totalPage = result.getTotalPage();
            // 判断是否还有spu没有查询
            if (page > totalPage) {
                // 没有则结束
                break;
            }
        }
    }

    @Override
    public Mono<List<String>> getSuggestion(String key) {
        if (StringUtils.isBlank(key)) {
            throw new LyException(400, "请求参数为null！");
        }
        Mono<List<String>> mono = goodsRepository.suggestBySingleField(SUGGESTION_FIELD, key);
        //利用suggestion查询自动补全
        mono.subscribe(list -> list.forEach(System.out::println));
        return mono;
    }

    @Override
    public Mono<PageInfo<Goods>> searchGoods(SearchParamDTO paramDTO) {

        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

        //1.source过滤
        //这里的资源应该在导入数据时候就排除需要的资源，这样字段少会提升性能
        sourceBuilder.fetchSource(new String[]{"id", "title", "price", "sold", "image"}, null);

        //2.查询条件 根据title查询
        //不能查询空格
        String key = paramDTO.getKey();
        if (StringUtils.isBlank(key)) {
            throw new LyException(404, "查询字段为空格");
        }

        //多个条件，使用boolquery查询
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        boolQuery.must(QueryBuilders.matchQuery("title", key));
        sourceBuilder.query(boolQuery);

        //3.分页
        Integer from = paramDTO.getPage();
        Integer size = paramDTO.getSize();
        from = (from == null || from < 0 || from > 100) ? 1 : from;
        size = (size == null || size < 0 || size > 100) ? 10 : size;
        sourceBuilder.from(from).size(size);

        //4.排序 true为
        if (StringUtils.isNotBlank(paramDTO.getSortBy())) {
            sourceBuilder.sort(paramDTO.getSortBy(), paramDTO.getDesc() ? SortOrder.DESC : SortOrder.ASC);
        }

        //5.高亮
        //注意这里的pretag和posttag不能与页面标签等关键字重复。
        HighlightBuilder highlightBuilder = new HighlightBuilder().field(DEFAULT_SEARCH_FIELD).preTags(DEFAULT_PRE_TAG).postTags(DEFAULT_POST_TAG);
        sourceBuilder.highlighter(highlightBuilder);

        Mono<PageInfo<Goods>> mono = goodsRepository.queryBySourceBuilderForPageHighlight(sourceBuilder);

        //在对象序列化为JSON的时候，忽略null字段,在yml中配置
        return mono;
    }

    @Override
    public void deleteGoodsById(Long id) {
        goodsRepository.deleteById(id);
    }

    @Override
    public void saveGoodsById(Long id) {
        //我们会在这里查出来所有的数据，因为rabbitmq是基于内存的，数据越小越好
        //查询spu
        SpuDTO spuDTO = itemClient.queryGoodsById(id);

        //构建goods
        Goods goods = buildGoods(spuDTO);

        //新增
        goodsRepository.save(goods);
    }

    //goods里面是用户用es查找的条件显示
    //页面中搜索需要的字段
    //页面展示需要的数据
    private Goods buildGoods(SpuDTO spu) {
        // 1.自动补全的提示字段
        //用户输入时候我们会拿该词语提示
        List<String> suggestion = new ArrayList<>();
        suggestion.add(spu.getName());
        suggestion.add(spu.getBrandName());
        Collections.addAll(suggestion, spu.getCategoryName().split("/"));

        // 2.sku的价格集合
        // 2.1.查询sku集合
        List<SkuDTO> skuList = spu.getSkus();
        if (CollectionUtils.isEmpty(skuList)) {
            // 没有sku，我们去查询
            skuList = itemClient.querySkuBySpuId(spu.getId());
        }
        // 2.2.获取价格集合
        Set<Long> prices = skuList.stream().map(SkuDTO::getPrice).collect(Collectors.toSet());

        // 3.商品销量
        //为该spu下的所有sku总和
        long sold = skuList.stream().mapToLong(SkuDTO::getSold).sum();

        // 4.sku的某个图片
        //图片存储可能有多个，使用，分隔，我们只去第一张
        String image = StringUtils.substringBefore(skuList.get(0).getImages(), ",");

        // 5.规格参数
        //保存为键值对形式，因为这样可以防止字段值越来越多
        List<Map<String, Object>> specs = new ArrayList<>();
        // 5.1.查询规格参数name和value键值对，只查询参与搜索的
        List<SpecParamDTO> params = itemClient.querySpecsValues(spu.getId(), true);
        // 5.2.封装
        for (SpecParamDTO param : params) {
            Map<String, Object> map = new HashMap<>(2);
            map.put("name", param.getName());
            map.put("value", chooseSegment(param));
            specs.add(map);
        }

        // 创建Goods对象，并封装数据
        Goods goods = new Goods();
        goods.setUpdateTime(new Date());
        // 自动补全的提示字段
        goods.setSuggestion(suggestion);
        // 规格参数
        goods.setSpecs(specs);
        // 商品销量
        goods.setSold(sold);
        // 商品标题
        goods.setTitle(spu.getTitle() + StringUtils.join(suggestion, " "));
        // sku的价格集合
        goods.setPrices(prices);
        // sku的某个图片
        goods.setImage(image);
        goods.setCategoryId(spu.getCid3());
        goods.setBrandId(spu.getBrandId());
        goods.setId(spu.getId());
        return goods;
    }

    //优化：将数据直接保存为区间范围，这样在查找时候比范围查找更加快
    private Object chooseSegment(SpecParamDTO p) {
        Object value = p.getValue();
        if (value == null || StringUtils.isBlank(value.toString())) {
            return "其它";
        }
        if (!p.getNumeric() || StringUtils.isBlank(p.getSegments()) || value instanceof Collection) {
            return value;
        }
        double val = parseDouble(value.toString());
        String result = "其它";
        // 保存数值段
        for (String segment : p.getSegments().split(",")) {
            String[] segs = segment.split("-");
            // 获取数值范围
            double begin = parseDouble(segs[0]);
            double end = Double.MAX_VALUE;
            if (segs.length == 2) {
                end = parseDouble(segs[1]);
            }
            // 判断是否在范围内
            if (val >= begin && val < end) {
                if (segs.length == 1) {
                    result = segs[0] + p.getUnit() + "以上";
                } else if (begin == 0) {
                    result = segs[1] + p.getUnit() + "以下";
                } else {
                    result = segment + p.getUnit();
                }
                break;
            }
        }
        return result;
    }

    private double parseDouble(String str) {
        try {
            return Double.parseDouble(str);
        } catch (Exception e) {
            return 0;
        }
    }
}